﻿
using EmperialApps.WeatherSpark.Data;
using EmperialApps.WeatherSpark.Internal;
using EmperialApps.WeatherSpark.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace EmperialApps.WeatherSpark {

    using CreateProportionalFigure = Func<double, double, double, double, PathFigure>;
    using RawDataValues = System.Collections.ObjectModel.ReadOnlyCollection<double>;

    /// <summary>Represents a control displaying a forecast data.</summary>
    public partial class WeatherControl : UserControl {

        /// <summary>Initializes a new instance of the <see cref="WeatherControl"/> class.</summary>
        public WeatherControl( ) { }


        /// <summary>Gets the interval for minor temperature grid lines.</summary>
        protected double TemperatureInterval {
            get {
                return this.Units == Units.Metric
                     ? MetricTemperatureInterval
                     : ImperialTemperatureInterval;
            }
        }

        /// <summary>Gets the interval for minor pressure grid lines.</summary>
        protected double PressureInterval {
            get {
                return this.Units == Units.Metric
                     ? MetricPressureInterval
                     : ImperialPressureInterval;
            }
        }


        /// <summary>Gets the root layout panel for the control.</summary>
        protected virtual Panel GetLayoutRoot( ) { return null; }

        /// <summary>Called when the visibility of child elements changes.</summary>
        protected virtual void OnVisibleElementsChanged( ) {
            this.DisplayForecast( );
        }


        /// <summary>Retrieves the temperature values for the specified forecast, using the configured units.</summary>
        protected DataValues GetTemperatureValues( Forecast forecast ) {
            return forecast.GetTemperatureValues( this.Units, ref this._temperatureValues );
        }

        /// <summary>Retrieves the wind speed values for the specified forecast, using the configured units.</summary>
        protected DataValues GetWindSpeedValues( Forecast forecast ) {
            return forecast.GetWindSpeedValues( this.Units, ref this._windSpeedValues );
        }

        /// <summary>Retrieves the pressure values for the specified forecast, using the configured units.</summary>
        protected DataValues GetPressureValues( Forecast forecast ) {
            return forecast.GetPressureValues( this.Units, ref this._pressureValues );
        }

        /// <summary>Coerces the data in the given forecast for display.</summary>
        protected virtual Forecast CoerceForecast( Forecast forecast ) {
            // If forecast does not have all data for the current day, prepend with blank data.
            var today = DateTimeOffset.Now.Date;
            if( today == forecast.Start.Date )
                forecast = CoerceForecastToWholeDay( forecast );

            return forecast;
        }

        /// <summary>Coerces the data in the given forecast to ensure it begins with a full day of values.</summary>
        public static Forecast CoerceForecastToWholeDay( Forecast forecast ) {
            var blankData = new Dictionary<ForecastData, RawDataValues>( );
            var blankValues = new RawDataValues( Enumerable.Repeat( double.NaN, ConvertValue.HoursPerDay / forecast.Interval ).ToArray( ) );
            for( ForecastData data = ForecastData.Temperature; data <= ForecastData.Last; data = (ForecastData)((int)data << 1) )
                if( Enum.IsDefined( typeof( ForecastData ), data ) )
                    blankData[data] = blankValues;

            var blankForecast = new Forecast( forecast.Location, forecast.Description, forecast.Start.GetDateOffset( ), forecast.Interval, blankData );
            forecast = Forecast.Combine( blankForecast, forecast );
            return forecast;
        }


        /// <summary>Displays the current forecast, if available.</summary>
        protected void DisplayForecast( ) {
            Forecast forecast = this.Forecast;
            if( forecast != null ) {
                this.DisplayForecastDescription( forecast );
                this.DisplayForecast( forecast, forecast.Start );
            }
        }

        /// <summary>Used to display the description for a forecast.</summary>
        protected virtual void DisplayForecastDescription( Place place, string nickname ) { }

        /// <summary>Used to display the data in the specified forecast.</summary>
        protected virtual void DisplayForecast( Forecast forecast, DateTimeOffset previousStart ) { }

        /// <summary>Used to display the night background for the specified temperature data.</summary>
        protected virtual void DisplayNightBackground( DataValues temperature ) { }

        /// <summary>Displays major and minor horizontal grid lines for the specified values.</summary>
        /// <returns>The largest value, and the largest grid line value.</returns>
        protected Pair<double, double?> DisplayGridLines( Path minorGridLines, Path majorGridLines, IEnumerable<double> values, Scale timeScale, Scale valueScale, double lineWidth ) {
            List<double> majorValues, minorValues;
            if( majorGridLines != null ) {
                values.SeparateMultiples( LargeTemperatureInterval ).GetValues( out majorValues, out minorValues );
            }
            else {
                majorValues = new List<double>( );
                minorValues = new List<double>( values );
            }

            var minorExtremes = minorGridLines.Update( Extensions.PathData, minorValues, ( t, geometry, vs ) =>
                this.DisplayTemperatureGridLines( geometry, vs, lineWidth, valueScale )
            );

            var majorExtremes = majorGridLines.Update( Extensions.PathData, majorValues, ( t, geometry, vs ) =>
                this.DisplayTemperatureGridLines( geometry, vs, lineWidth, valueScale )
            );

            double maximumPosition = Math.Max( minorExtremes.Item2, majorExtremes.Item2 );
            double? extremeTemperature = minorExtremes.Item1 ?? majorExtremes.Item1;
            return Pair.Create( maximumPosition, extremeTemperature );
        }

        /// <summary>Displays major and minor horizontal grid lines for the specified temperature values.</summary>
        /// <returns>The largest temperature value.</returns>
        protected double DisplayTemperatureGridLines(
            Path minorTemperatureGridLines, Path majorTemperatureGridLines, Line extremeTemperatureLine,
            IEnumerable<double> values, Scale timeScale, Scale temperatureScale, double lineWidth ) {

            double maximumPosition;
            double? extremeTemperature;
            this.DisplayGridLines( minorTemperatureGridLines, majorTemperatureGridLines, values, timeScale, temperatureScale, lineWidth ).GetValues( out maximumPosition, out extremeTemperature );
            DisplayExtremeTemperatureLine( extremeTemperatureLine, timeScale.ScreenSize, extremeTemperature, temperatureScale );

            return maximumPosition;
        }


        /// <summary>Updates the clip mask for an element to match its screen size.</summary>
        protected static void ClipToSize( FrameworkElement element ) {
            element.Update(
                Extensions.RectangleClip,
                ( e, r ) => r.Rect = new Rect( 0, 0, e.ActualWidth, e.ActualHeight ) );
        }

        /// <summary>Updates the clip mask for the night background to match the specified temperature data.</summary>
        protected void ClipNightBackground( PathGeometry clip, DataValues temperature, Scale timeScale, Scale temperatureScale ) {
            Display.ClipNightBackground( clip, this.Forecast.Location, this.Forecast.Start, temperature, timeScale, temperatureScale, this.ShowExtremesUsingNight );
        }

        /// <summary>Updates the clip mask for an element to match the forecast data using the specified figure.</summary>
        protected static void ClipTrace( FrameworkElement trace, DataValues dataValues, CreateProportionalFigure createProportionalFigure, Scale timeScale, int span ) {
            trace.Update( Extensions.PathClip, Pair.Create( Pair.Create( timeScale, span ), Pair.Create( dataValues, createProportionalFigure ) ),
                ( t, clip, data ) => {
                    Scale scale = data.Item1.Item1;
                    int targetSpan = data.Item1.Item2;
                    double fullSize = scale.ToScreen( targetSpan ) - scale.ToScreen( 0 );
                    double y = t.ActualHeight / 2;

                    var values = data.Item2.Item1;
                    var createFigure = data.Item2.Item2;
                    int averageSpan = Math.Max( targetSpan / values.Interval, 1 );
                    int positionOffset = averageSpan != targetSpan ? targetSpan - 1 : 0;
                    foreach( var pair in Average( values, averageSpan ) ) {
                        double x = scale.ToScreen( pair.Item1 + positionOffset );
                        var figure = createFigure( fullSize, pair.Item2, x, y );
                        clip.Figures.Add( figure );
                    }
                }
            );
        }


        /// <summary>Creates a horizontal line.</summary>
        protected static PathFigure CreateHorizontalLineFigure( double left, double right, double y ) {
            y = Math.Round( y );
            var start = new Point( left, y );
            var segment = new LineSegment { Point = new Point( right, y ) };
            return new PathFigure { StartPoint = start, Segments = { segment } };
        }

        /// <summary>Creates a vertical line.</summary>
        protected static PathFigure CreateVerticalLineFigure( double x, double top, double bottom ) {
            x = Math.Round( x );
            var start = new Point( x, top );
            var segment = new LineSegment { Point = new Point( x, bottom ) };
            return new PathFigure { StartPoint = start, Segments = { segment } };
        }

        /// <summary>Creates a square with an area proportional to a full-sized figure.</summary>
        protected static PathFigure CreateProportionalSquare( double fullSize, double proportion, double x, double y ) {
            // Calculate the side length so that the square's area is proportional to the area of the full-size square.
            double length = Math.Sqrt( proportion * fullSize * fullSize );
            double halfLength = length / 2;

            return Display.CreateRectangleFigure( x - halfLength, x + halfLength, 0, length );
        }

        /// <summary>Creates a circle with an area proportional to a full-sized figure.</summary>
        protected static PathFigure CreateProportionalCircle( double fullDiameter, double proportion, double x, double y ) {
            // Calculate the radius so that the circle's area is proportional to the area of the full-size circle.
            //  A = πr² => πr² = pπR² => r = √(pπR²/π) = √(pR²) = √(p)R = √(p)D/2
            double radius = Math.Sqrt( proportion ) * fullDiameter / 2;

            var size = new Size( radius, radius );
            var top = new Point( x, y - radius );
            var bottom = new Point( x, y + radius );

            var topArc = new ArcSegment { Point = bottom, Size = size, RotationAngle = 180 };
            var bottomArc = new ArcSegment { Point = top, Size = size, RotationAngle = 180 };
            return new PathFigure { StartPoint = top, Segments = { topArc, bottomArc } };
        }


        /// <summary>Manages the generation and positioning of a set of visual elements in a grid along a scale.</summary>
        protected abstract class VisualManager<T> : ItemManager<T>
            where T : FrameworkElement {
            private readonly Grid _panel;
            private readonly Func<object> _createVisual;
            private readonly DependencyProperty _transformProperty;

            protected VisualManager( Func<object> createVisual, Grid panel, bool isHorizontal, double overhangAllowance )
                : base( overhangAllowance ) {
                this._panel = panel;
                this._createVisual = createVisual;
                this._transformProperty =
                    isHorizontal
                        ? TranslateTransform.XProperty
                        : TranslateTransform.YProperty;
            }

            public bool IsHorizontal {
                get { return object.ReferenceEquals( this._transformProperty, TranslateTransform.XProperty ); }
            }

            public virtual IList<double> DisplayVisuals( Scale scale, double increment, double offset ) {
                double displayLimit = this.IsHorizontal ? this._panel.ActualWidth : this._panel.ActualHeight;
                return this.DisplayItems( scale, increment, offset, displayLimit );
            }

            protected override T CreateItem( ) {
                var item = (T)this._createVisual( );
                this._panel.Children.Add( item );
                return item;
            }

            protected override void SetItemPosition( T item, double itemPosition ) {
                item.Translate( this._transformProperty, itemPosition );
            }

            protected override void SetItemVisibility( T item, bool visible ) {
                item.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
            }

            protected override void SetItemValue( T item, double value ) {
                item.Tag = value;
                item.DataContext = value;
            }

            protected override double GetItemValue( T item ) {
                return (double)item.Tag;
            }

            protected override double GetItemSize( T item ) {
                return this.IsHorizontal
                     ? item.ActualWidth
                     : item.ActualHeight;
            }
        }

        /// <summary>Manages the generation and positioning of a set of text labels along a scale.</summary>
        protected class LabelManager : VisualManager<FrameworkElement> {
            public LabelManager( DataTemplate labelTemplate, Grid labelPanel, bool isHorizontal, double overhangAllowance )
                : base( labelTemplate.LoadContent, labelPanel, isHorizontal, overhangAllowance ) { }

            protected override double GetItemCenteringOffset( FrameworkElement item ) {
                return this.IsHorizontal
                     ? item.ActualWidth / 2
                     : item.ActualHeight * 2 / 5;
            }
        }

        /// <summary>Manages the generation and positioning of a set of time labels along a scale.</summary>
        protected sealed class TimeLabelManager : LabelManager {
            private DateTimeOffset _start;
            private double _valueOffset;

            public TimeLabelManager( DataTemplate labelTemplate, Grid labelPanel, double overhangAllowance )
                : base( labelTemplate, labelPanel, true, overhangAllowance ) {
                this.ShowHour = true;
            }

            public bool ShowHour { get; set; }

            public Thickness Margin { get; set; }

            public string LabelOverride { get; set; }

            public DateTimeOffset Start {
                get { return this._start; }
                set {
                    var previousStart = this._start;
                    this._start = value;

                    if( previousStart != default( DateTimeOffset ) )
                        this._valueOffset =
                            this.ShowHour
                                ? previousStart.Hour - value.Hour
                                : (previousStart - value).TotalHours;
                }
            }

            public override IList<double> DisplayVisuals( Scale scale, double increment, double offset ) {
                var displayValues = base.DisplayVisuals( scale, increment, offset );
                this._valueOffset = 0;
                return displayValues;
            }

            protected override double GetRangeStart( double minimum, double increment ) {
                var time = Start.AddHours( minimum );
                double start = base.GetRangeStart( time.Hour, increment ) - time.Hour;
                return start;
            }

            protected override void SetItemValue( FrameworkElement item, double value ) {
                item.Tag = value;
                var time = Start.AddHours( value );
                string label =
                    this.LabelOverride != null ? time.ToString( this.LabelOverride ) :
                    this.ShowHour ? time.ToString( "ht" ).ToLowerInvariant( ) :
                    time.Date == DateTimeOffset.Now.Date ? AppResources.Graph_DateToday :
                    time.ToString( AppResources.Graph_DateFormat );

                item.DataContext = label;
            }

            protected override double GetItemValue( FrameworkElement item ) {
                double value = base.GetItemValue( item );
                double offsetValue = value + this._valueOffset;
                item.Tag = offsetValue;
                return offsetValue;
            }

            protected override void SetItemPosition( FrameworkElement item, double itemPosition ) {
                base.SetItemPosition( item, itemPosition );
                item.Margin = this.Margin;
            }

            protected override double GetItemCenteringOffset( FrameworkElement item ) {
                return this.ShowHour
                     ? base.GetItemCenteringOffset( item )
                     : 0;
            }
        }


        #region Private Members

        private const double MetricTemperatureInterval = 5;
        private const double ImperialTemperatureInterval = 10;
        private const double LargeTemperatureInterval = MetricTemperatureInterval * 4;

        private const double MetricPressureInterval = 20;
        private const double ImperialPressureInterval = 2;

        private readonly Dictionary<ForecastDisplayLevel, DisplayLevelElements> _levelElements = new Dictionary<ForecastDisplayLevel, DisplayLevelElements>( );
        private DataValues _temperatureValues;
        private DataValues _windSpeedValues;
        private DataValues _pressureValues;


        private static IEnumerable<Pair<int, double>> Average( DataValues values, int span ) {
            double total = 0;
            for( int i = 0; i < values.Count; ++i ) {
                total += values[i];
                if( total == 0 || (i + 1) % span != 0 )
                    continue;

                yield return Pair.Create( i * values.Interval, total / span );
                total = 0;
            }
        }


        private void OnDisplayLevelChanged( ForecastDisplayLevel oldLevel, ForecastDisplayLevel newLevel ) {
            // Find all tagged visual elements.
            if( this._levelElements.Count == 0 )
                DisplayLevelElements.Find( this._levelElements, this.GetLayoutRoot( ), recurse: true );

            // Update visibility of any level elements that have changed.
            bool change = false;
            for( ForecastDisplayLevel level = ForecastDisplayLevel.TemperatureSummary;
                 (level & ForecastDisplayLevel.DetailsMask) != 0;
                 level = (ForecastDisplayLevel)((int)level << 1) ) {
                bool wasVisibile = (level & oldLevel) != 0;
                bool isVisible = (level & newLevel) != 0;
                if( wasVisibile != isVisible ) {
                    DisplayLevelElements elements;
                    if( this._levelElements.TryGetValue( level, out elements ) )
                        change = elements.UpdateVisibility( isVisible );
                }
            }

            if( change ) {
                this.UpdateLayout( );
                this.OnVisibleElementsChanged( );
            }
        }

        private void OnUnitsChanged( ) {
            this.DisplayForecast( );
        }

        private void OnShowExtremesUsingNightChanged( ) {
            Forecast forecast = this.Forecast;
            if( forecast != null ) {
                var temperature = this.GetTemperatureValues( forecast );
                this.DisplayNightBackground( temperature );
            }
        }

        private void OnNicknameChanged( ) {
            Forecast forecast = this.Forecast;
            if( forecast != null )
                this.DisplayForecastDescription( forecast );
        }

        private void OnForecastChanged( Forecast oldForecast, Forecast newForecast ) {
            DateTimeOffset start = newForecast.Start;
            if( oldForecast != null ) {
                if( oldForecast.Start < start )
                    start = oldForecast.Start;
                else if( oldForecast.Temperature.SequenceEqual( newForecast.Temperature ) )
                    return;
            }

            this.DisplayForecastDescription( newForecast );
            this.DisplayForecast( newForecast, start );
        }

        private static object ForecastCoerce( DependencyObject d, object baseValue ) {
            var self = (WeatherControl)d;
            var forecast = self.CoerceForecast( (Forecast)baseValue );
            return forecast;
        }


        private void DisplayForecastDescription( Forecast forecast ) {
            Place place = Place.Create( this.Units, forecast.Location, forecast.Description );
            string nickname = this.Nickname ?? place.Name;

            this.DisplayForecastDescription( place, nickname );
        }

        private Pair<double?, double> DisplayTemperatureGridLines( PathGeometry geometry, IEnumerable<double> values, double lineWidth, Scale temperatureScale ) {
            double low, high;
            this.Units.GetExtremeTemperatures( out low, out high );

            double? extremeValue = null;
            double maximumPostion = double.NegativeInfinity;
            foreach( double value in values ) {
                double linePostion = Math.Round( temperatureScale.ToScreen( value ) );
                maximumPostion = Math.Max( maximumPostion, linePostion );

                geometry.Figures.Add( CreateHorizontalLineFigure( 0, lineWidth, linePostion ) );

                if( value == low || value == high )
                    extremeValue = value;
            }

            return Pair.Create( extremeValue, maximumPostion );
        }

        private static void DisplayExtremeTemperatureLine( Line extremeTemperatureLine, double lineWidth, double? extremeValue, Scale temperatureScale ) {
            if( extremeValue.HasValue ) {
                double linePosition = Math.Round( temperatureScale.ToScreen( extremeValue.Value ) );

                extremeTemperatureLine.X2 = lineWidth;
                extremeTemperatureLine.Y1 = extremeTemperatureLine.Y2 = linePosition;
                extremeTemperatureLine.Stroke =
                    extremeValue < 35
                        ? new SolidColorBrush( Color.FromArgb( 0xFF, 0x11, 0x11, 0xEE ) )   // #F11E
                        : new SolidColorBrush( Color.FromArgb( 0xFF, 0xEE, 0x11, 0x11 ) );  // #FE11
            }
            else {
                extremeTemperatureLine.Stroke = null;
            }
        }

        private sealed class DisplayLevelElements {
            public override string ToString( ) { return GetType( ).Name + ": Count=" + this._elements.Count; }

            public void Add( UIElement child ) { this._elements.Add( child ); }
            public void Add( RowDefinition row ) { this._elements.Add( row ); }

            public bool UpdateVisibility( bool visible ) {
                object update = null;
                foreach( var element in this._elements ) {
                    update = this.UpdateVisibility( element as UIElement, visible )
                          ?? this.UpdateVisibility( element as RowDefinition, visible )
                          ?? update;
                }

                return update != null;
            }

            public static void Find( Dictionary<ForecastDisplayLevel, DisplayLevelElements> levelElements, Panel panel, bool recurse ) {
                if( panel == null )
                    return;

                Grid grid = panel as Grid;
                if( grid != null )
                    foreach( RowDefinition row in grid.RowDefinitions ) {
                        ForecastDisplayLevel rowLevel = GetLevel( row );
                        if( rowLevel != ForecastDisplayLevel.None )
                            EnsureEntry( levelElements, rowLevel ).Add( row );
                    }

                foreach( UIElement child in panel.Children ) {
                    ForecastDisplayLevel childLevel = GetLevel( child );
                    if( childLevel != ForecastDisplayLevel.None )
                        EnsureEntry( levelElements, childLevel ).Add( child );
                    else if( recurse )
                        Find( levelElements, child as Panel, recurse: false );
                }
            }

            #region Private Members

            public static readonly DependencyProperty DesiredMaxHeightProperty = DependencyProperty.RegisterAttached(
                "DesiredMaxHeight", typeof( double ), typeof( DisplayLevelElements ), new PropertyMetadata( double.PositiveInfinity ) );

            private readonly List<DependencyObject> _elements = new List<DependencyObject>( );

            private object UpdateVisibility( UIElement child, bool visible ) {
                if( child != null )
                    child.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;

                return child;
            }

            private object UpdateVisibility( RowDefinition row, bool visible ) {
                if( row != null )
                    if( visible ) {
                        row.MaxHeight = (double)row.GetValue( DesiredMaxHeightProperty );
                    }
                    else {
                        row.SetValue( DesiredMaxHeightProperty, row.MaxHeight );
                        row.MaxHeight = 0.0;
                    }

                return row;
            }

            private static DisplayLevelElements EnsureEntry( Dictionary<ForecastDisplayLevel, DisplayLevelElements> levelElements, ForecastDisplayLevel level ) {
                DisplayLevelElements elements;
                if( !levelElements.TryGetValue( level, out elements ) )
                    levelElements.Add( level, elements = new DisplayLevelElements( ) );

                return elements;
            }

            #endregion
        }

        #endregion

    }

}
